Add replay protection to encrypted commands (MJ-11)#5
Open
balloob wants to merge 2 commits into
Open
Conversation
The inbound encrypted-command path only verified session liveness and the CCM tag, never the nonce counter. An attacker who captured any encrypted command frame in a live session could replay it verbatim and the device would re-execute it. The replay-window fields already existed in the session struct but were unused on the inbound path. Port the nRF firmware's verifyNonceReplay sliding-window check to Silabs and wire it into decrypt_encrypted_payload at the point the CCM tag is verified, mirroring nRF's decryptCommand semantics so both firmwares behave identically against the same client: reject replays (already-seen or below the +/-32 window), advance last_seen_counter/replay_window on accept, and clear the session after repeated integrity failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
The replay counter is read from the cleartext nonce, so trusting it before the CCM tag is verified let a forged frame advance last_seen_counter and the replay window, then fail the tag. Chained forged frames (each within the +/-32 window) walk the window arbitrarily far forward, after which legitimate lower-counter frames fall outside the window and are rejected as replays -- a session denial-of-service. Split the check into two steps mirroring the nRF companion reorder (OpenDisplay/Firmware_NRF fix/replay-window-advance-after-auth): nonce_replay_check rejects replays/out-of-window nonces before decrypt but no longer mutates state; nonce_replay_advance records the counter and is called only after od_ccm_decrypt confirms the tag. A forged frame with a bad tag now returns before advancing and cannot desync the window, while a legitimate in-order frame still advances it exactly once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Silabs inbound encrypted-command path performs no replay protection.
decrypt_encrypted_payloadonly checkedsession_alive()and the CCM tag; the replay-window fields in the session struct (last_seen_counter,replay_window,replay_idx) were never consulted on the inbound path. An attacker who captures any encrypted command frame in a live session can replay it verbatim and the device re-executes it.The nRF firmware already validates a sliding nonce-counter window (
encryption.cverifyNonceReplay, called fromdecryptCommand), so the two firmwares diverged in security behavior.Fix (mirrors nRF)
Port nRF's
verifyNonceReplayto Silabs asverify_nonce_replayand wire it intodecrypt_encrypted_payloadat the point the CCM tag is verified, matching nRF'sdecryptCommandsemantics so both firmwares behave identically against the same client:last_seen_counterand already present in the 64-entryreplay_window.last_seen_counterwhen the counter is newer, and record the counter in the circularreplay_window.integrity_failuresand clear the session after 3, mirroring nRF; resetintegrity_failuresto 0 on a successful decrypt.This uses the existing nonce counter only — the nonce direction-bit change (MJ-12) is a separate breaking change handled elsewhere and is intentionally not touched here. No other command handling is modified.
Testing
Full SLC/Simplicity firmware build could not be run in this environment (no
arm-none-eabi-gcc/slctoolchain available). Verification done:EncryptionSessionwith host GCC (-Wall -Wextra -std=c11): clean, no warnings; all referenced struct fields exist and types match.verifyNonceReplayaccept/reject decisions: fresh counter accepted, out-of-order replay below the window rejected, counter far outside the +/-32 window rejected, session_id mismatch rejected. The one nRF quirk (a replay of the exact currentlast_seen_counter, diff==0, is accepted until a newer counter arrives) is reproduced identically, keeping the two firmwares in lockstep.🤖 Generated with Claude Code
https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
Replay-ordering companion: OpenDisplay/Firmware_NRF#5 (same advance-after-auth reorder). This PR now includes both the replay protection (MJ-11) and the advance-after-tag ordering.